iT邦幫忙

2023 iThome 鐵人賽

DAY 5
4

簡介

經常有人說 ChatGPT 是在「一本正經的胡言亂語」,在 GPT-4 推出之後,尤其能感受到 GPT-3.5 相對容易產生錯誤。但是喜歡胡言亂語未必是個缺點!相信大家在日常生活中,也經常遇到喜歡胡言亂語的人,但他們也都活的好好的。

筆者過去也擔任過一陣子專門「胡言亂語」的塔羅占卜場外人,因此我們就來用這個喜歡「胡言亂語」的模型打造一個喜歡「胡言亂語」的 AI 塔羅占卜師吧!

註:並非所有塔羅占卜都是胡言亂語,只是因為筆者學藝不精,多被友人評為胡言亂語。占星學博大精深,筆者相當敬重深入研究這門學問的占卜師。

可愛貓貓 Day 5

(Powered By Microsoft Designer)

Gradio

首先先來介紹Gradio這款在機器學習領域相當受歡迎的套件,只需要簡單幾行程式碼就快速搭建一個美美的網頁應用給別人看,完全不需要煩惱前端技術、同步非同步的問題,非常適合快速展示概念驗證。

Gradio 於 2019 年創立,由 Stanford 的 PhD Abubakar Abid 與幾位室友一起合作的專案,在 2021 年 12 月加入 Hugging Face 家族。目的在於快速搭建一個模型的應用,讓使用者易於展示與操作,更棒的是還支援護眼的深色主題!

對於筆者這種長期活在 Python 深井,一輩子沒看過前端三神獸 HTML, CSS, JavaScript 的開發者而言,是個相當友善的框架。Gradio 支援的面向相當廣泛,本文主要介紹顯示塔羅牌用的圖片元件,以及對話用的聊天室元件。

gr.Blocks 基本用法

Gradio 官方提供相當簡潔的 Interface 類別,可以用三行程式碼搞定一個基本介面。但如果要細緻一點的排版,通常會用到 Block Demo 的形式,以下是一個簡單的範例:

import gradio as gr

with gr.Blocks() as demo:
    inn = gr.Textbox(label="輸入")
    out = gr.Textbox(label="輸出")

    def foo(inn: str):
        return inn.swapcase()

    inn.change(foo, inn, out)

demo.launch()

執行此程式碼後,會在 http://127.0.0.1:7860 建立一個服務,連上去就是我們搭建出來的介面,使用起來大致如下:

Demo01

這段程式會將上方輸入的英文字母做大小寫反轉,然後將結果放在輸出框裡面。這簡單幾行程式碼,有輸入、有輸出、有介面,終於不用再給教授或主管看小黑窗看到眼花啦!Gradio 也提供一個簡單的參數,讓你可以快速的把 Demo 短暫公開出去:

demo.launch(share=True)

啟動之後等一段時間,就會看到以下訊息:

Running on local URL:  http://127.0.0.1:7860
Running on public URL: https://xxxxxxxxxxxxxxxxxx.gradio.live

這樣就可以讓親朋好友透過 Public URL 連上你的 Demo 試玩了!但偶爾還是會有穿不出去的網路情況,這時可以考慮使用 Ngrok 之類的工具幫你 Tunnel 出去。

gr.Image 圖片元件

Image 元件的用法相當單純,只要給他圖片的 URL 或檔案路徑,他就能幫你把圖片顯示出來,例如:

import gradio as gr

with gr.Blocks() as demo:
    url = "https://i.postimg.cc/NMb4ZQ8b/Happy-Fries.webp"
    img = gr.Image(url, height=256, width=256)

demo.launch()

以上程式碼會展示一張可愛的貓貓圖,並將高度與寬度限制為 256。

gr.Chat 聊天室元件

基本用法

Chat 元件使用字串的二維陣列來表示歷史聊天紀錄,陣列裡面的每個元素代表每回合的對話。每回合又包含兩個字串,第一個字串會是從右邊冒出來的對話泡泡,第二個則是左邊。如果其值為 None 則不會顯示任何對話泡泡,以下是個基本的範例:

import random
import gradio as gr

def send_msg(msg: str, chat: list):
    r1 = random.randint(1, 5)  # "喵" 的數量
    r2 = random.randint(1, 3)  # "!" 的數量
    resp = "喵" * r1 + "!" * r2
    chat.append([msg, resp])
    return None, chat

with gr.Blocks() as demo:
    chat = gr.Chatbot(label="喵星人", height=290)
    msg = gr.Textbox(label="學貓叫")
    msg.submit(send_msg, [msg, chat], [msg, chat])

demo.launch()

使用者送出訊息後(按 Enter 鍵)會回覆隨機數量的「喵」,看起來會像這樣:

Demo

串流訊息

Gradio 透過 Python Generator 的方式來對元件進行訊息串流,以下是個簡單範例:

import random
import time
import gradio as gr

def get_resp():
    r1 = random.randint(1, 5)  # "喵" 的數量
    r2 = random.randint(1, 3)  # "!" 的數量
    resp = "喵" * r1 + "!" * r2

    for ch in resp:
        time.sleep(0.3)  # 模擬文字傳遞間的延遲
        yield ch

def send_msg(msg: str, chat: list):
    resp = get_resp()
    chat.append([msg, None])
    return None, chat, resp

def show_resp(chat: list, resp):
    chat[-1][1] = ""
    for ch in resp:
        chat[-1][1] += ch
        yield chat

with gr.Blocks() as demo:
    resp = gr.State(None)
    chat = gr.Chatbot([[None, "喵!"]], label="喵星人", height=290)
    msg = gr.Textbox(label="學貓叫")

    msg.submit(
        send_msg, [msg, chat], [msg, chat, resp]
    ).then(show_resp, [chat, resp], chat)

demo.queue().launch()

我們將隨機產生喵喵喵的函式拉出來,並使用 time.sleep 模擬訊息串流間的延遲。當訊息被送出 (submit) 的時候,會從 get_resp 取得一個 Generator 並放在一個 gr.State 裡面。

放在 gr.State 裡面的物件,會根據不同的對話 Session 而被複製,用來控管每個使用者各自獨立的資料。放在 gr.State 裡面的物件的使用方式與原本的物件相同。例如其中存放的是字串,你就可以當成一般的字串來操作。這邊我們放的是 Response 的 Generator 物件。

這個 Generator 會經過 .then 進到 show_resp 裡面,在這裡面慢慢把訊息展開並輸出。這邊要注意,每次 yield 都要給完整的內容,而不是只有新增的文字而已。執行結果如下圖:

Demo

停止串流

有啟動串流,就有停止串流。這裡我們需要借助 Python 內建套件的 threading.Event 類別。這個 Event 可以發出訊號,讓串流的 Function 可以捕捉此訊號,並進行停止的動作,請參考以下範例:

import random
import time
from threading import Event

import gradio as gr


def get_resp():
    r1 = random.randint(10, 20)  # "喵" 的數量
    r2 = random.randint(1, 3)  # "!" 的數量
    resp = "喵" * r1 + "!" * r2

    for ch in resp:
        time.sleep(0.3)  # 模擬文字傳遞間的延遲
        yield ch


with gr.Blocks() as demo:
    event = gr.State(None)
    resp = gr.State(None)
    chat = gr.Chatbot([[None, "喵!"]], label="喵星人", height=230)
    msg = gr.Textbox(label="學貓叫")
    stop = gr.Button("冷靜!")

    def send_msg(msg: str, chat: list):
        resp = get_resp()
        chat.append([msg, None])
        return None, chat, resp, Event()

    def show_resp(chat: list, resp, event: Event):
        chat[-1][1] = ""
        for ch in resp:
            if event.is_set():
                event.clear()
                chat[-1][1] += " ..."
                chat.append(["冷靜!", "好吧"])
                yield chat
                break

            chat[-1][1] += ch
            yield chat

    def stop_show(event: Event):
        event.set()

    msg.submit(send_msg, [msg, chat], [msg, chat, resp, event]).then(
        show_resp, [chat, resp, event], chat
    )
    stop.click(stop_show, event, queue=False)

demo.queue().launch()

我們同樣透過 gr.State 來存放 Event 物件,避免這邊按下停止,全世界的訊息都被擋下來。但是要特別注意,這個 Event 的底層實做因為涉及 C 函式庫的關係,所以無法直接被 gr.State 複製。因此初始化時我們不直接放一個 Event() 在裡面,而是先用 None 去做初始化,在 send_msg 時再把 Event() 設定進去。

show_resp 的迴圈裡面會不斷檢查 event.is_set(),如果使用者按下 Stop 而觸發了 event.set(),那 show_resp 就會跳出迴圈,從而達到停止訊息的效果。

Demo

貓貓塔羅

首先附上貓主子精神抖擻的照片鎮樓:

Fries

因為貓貓很可愛,所以就讓他們來占卜吧!

接下來,我們使用 ChatGPT API 與 Gradio 搭建一個貓貓塔羅的應用。

素材蒐集

首先我們需要塔羅牌的牌面圖片,這部份可以參考此 GitHub 專案,裡面提供了完整的 78 張偉特塔羅牌圖片,授權也是免費使用的。但是抽塔羅牌時,會有正反向的問題,因此也需要準備另外 78 張旋轉 180 度的圖片:

from PIL import Image

img = Image.open("image.png").rotate(180)
img.save("image_rotated.png")

除了圖片以外,我們還需要解牌資訊。這是筆者多年前從樂樂小棧上爬取下來的資料,可惜該網站似乎已經倒閉了 QQ

有了素材之後,我們就可以開始著手建構核心應用的部份了!

核心應用

首先,我們要先設計 System Prompt 的部份。一開始先從「人設」著手,使用俗稱「催眠」的手段來進行:

你現在是一個專業的塔羅牌占卜師,而且你的身份是貓咪,所以你會使用很多「喵喵」做為句末助詞以及口頭禪。

接著,我們要告訴 ChatGPT 需要進行的任務:

我會輸入一個問題,以及一張塔羅牌,你必須根據這張塔羅牌所代表的涵義,針對提出的問題給出詳細的解釋。

因為塔羅牌裡面也有滿多看起來「不好」的牌,為了避免讓使用者心生陰影,可以要求 ChatGPT 往積極樂觀的方向做解釋:

在解釋問題時,請盡量往正面、積極的方向做解釋,並鼓勵對方。

在系統提示中,我們可以要求 ChatGPT 隱藏自己的身份。另外我們只做單輪對話,所以也要避免 ChatGPT 企圖開啟第二輪對話:

在這個過程中,你不能透露你是 AI,也不能透露你是語言模型,也不要提及你的身份,也不要向我要求更多訊息。

如果不這樣設限,ChatGPT 可能會試圖要求更多資訊以提供更精確的答案。最後再加上一些額外設定:

解釋完之後要用「喵喵解牌完畢!」做結尾。請使用繁體中文。

這樣我們大致上就完成了這個系統提示的設計了。接著,我們將占卜的相關資訊也放進 Prompt 裡面,例如:

問題:我能穿越到異世界開掛嗎
塔羅牌:正向愚者
相關詞:天真、單純、可能、流浪、自由、隨興、古怪、輕浮、妄想、浪費、瘋狂、無知。
解牌開始:

我們可以先在 ChatGPT 的網頁介面測試一下,請參考此對話連結。可以看到,如果是像筆者一樣不會閱讀空氣的人,看到這些相關詞可能早就回答「有夢最美,希望相隨,早點去睡」了。但是 ChatGPT 硬是把這個問題解釋的充滿夢想、十分勵志的感覺,也許 ChatGPT 不能取代專業的占卜師,但至少可以取代我 XD

初步的 Demo 程式碼如下:

from openai import OpenAI

problem = "我能穿越到異世界開掛嗎"
name = "正向愚者"
related = "天真、單純、可能、流浪、自由、隨興、古怪、輕浮、妄想、浪費、瘋狂、無知。"

system_prompt = "你現在是一個專業的塔羅牌占卜師,而且你的身份是貓咪,所以你會使用很多「喵喵」做為句末助詞以及口頭禪。我會輸入一個問題,以及一張塔羅牌,你必須根據這張塔羅牌所代表的涵義,針對提出的問題給出詳細的解釋。在解釋問題時,請盡量往正面、積極的方向做解釋,並鼓勵對方。在這個過程中,你不能透露你是 AI,也不能透露你是語言模型,也不要提及你的身份,也不要向我要求更多訊息。解釋完之後要用「喵喵解牌完畢!」做結尾。請使用繁體中文。"
user_prompt = f"問題:{problem}\n塔羅牌:{name}\n相關詞:{related}\n解牌開始:"

client = OpenAI()
response = client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=[
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_prompt},
    ],
    stream=True,
)

for resp in response:
    if resp.choices[0]:
        print(end=resp.choices[0].delta.content, flush=True)
print()

完整應用

抽牌的實做相當單純也相對瑣碎,就不另外花費篇幅講解。筆者將完整的程式碼放在此專案,有興趣的朋友可以參考看看。筆者另外有一個 Discord 機器人,裡面也整合了類似的應用,可以透過 /超級薯條塔羅 這個指令來使用,還請各位多多支持這個小機器人!

結論

其實塔羅占卜不僅只是胡言亂語而已,專業的占卜師對塔羅牌的**「形」「意」**是相當注重的。「形」指的是塔羅牌的樣貌,「意」指的是塔羅牌的含義。現在的語言模型不僅能透過強大的文字理解能力來解釋塔羅牌的「意」,也能透過多模態的圖片理解能力來解讀塔羅牌的「形」。

在解讀塔羅牌時,並非單純只是照本宣科,依照解牌書逐字朗讀而已。很多時候會因為被占卜人的問題、生活、背景與個性,而有不同的解讀方式。但更重要的是,要為這些向塔羅牌求助的人點亮一盞指引方向的「明燈」。

很顯然,ChatGPT並沒有辦法做到這麼深入,因為ChatGPT無法真正「認識」你。對開發者而言,能夠做的是把模型的解讀引領到正面積極的方向,雖然未必能擔任「明燈」的角色,但如果能對使用者起到一絲絲一點點鼓勵的作用,那也算是有意義了。

搭建過這個簡單的應用後,筆者認為諸如宮廟、教會等宗教產業,可能是最不容易被人工智慧取代的職業吧!畢竟心靈的開導還是很講究「人情的溫暖」,無論是心靈的溫度還是燒金紙的溫度,都是機器無法取代的!

參考


上一篇
LLM Note Day 4 - OpenAI API
下一篇
LLM Note Day 6 - ChatGPT 的挑戰者們
系列文
LLM 學習筆記33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言